package com.gui.textFields;

import com.gui.interfaces.HighlightableNotEditable;
import java.awt.Color;
import java.awt.Font;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.DefaultHighlighter;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.Element;
import javax.swing.text.Highlighter;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
import javax.swing.text.StyledDocument;
import javax.swing.text.TabSet;
import javax.swing.text.TabStop;
import javax.swing.text.Utilities;
import javax.swing.undo.UndoManager;

/**
 * @author sad
 */
public class AbstractTextField extends JTextPane implements HighlightableNotEditable {

    protected UndoManager undo = new UndoManager();
    final private StyledDocument document;
    final private Map<String, LinePainter> painters = new HashMap<String, LinePainter>();
    final private Map<String, Object> highlighters = new HashMap<String, Object>();

    public AbstractTextField() {
        setFont(new Font("Monospaced", 0, 13));
        Color lineColor = new Color(100, 100, 255, 20);
        new LineCaretPainter(this, lineColor);

        document = new DefaultStyledDocument() {
            @Override
            public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
                if ("\t".equals(str)) {
                    str = "    ";
                }
                super.insertString(offs, str, a);
            }
        };

        setFont(new Font("Monospaced", 0, 13));
        setDocument(document);
        StyleContext sc = StyleContext.getDefaultStyleContext();
        TabSet tabs = new TabSet(new TabStop[]{new TabStop(20)});
        AttributeSet paraSet = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.TabSet, tabs);
        setParagraphAttributes(paraSet, false);
        document.putProperty(DefaultEditorKit.EndOfLineStringProperty, System.getProperty("line.separator"));
        document.addUndoableEditListener(new MyUndoableEditListener());
        document.addDocumentListener(new DocumentListener() {
            @Override
            public void insertUpdate(DocumentEvent e) {
                setModified(true);
                if (e.getLength() == 1) {
                    try {
                        String change = getText(e.getOffset(), e.getLength());
                        if ("\n".equals(change)) {
                            SwingUtilities.invokeLater(new Runnable() {
                                @Override
                                public void run() {
                                    try {
                                        newRow();
                                    } catch (BadLocationException ex) {
                                        throw new RuntimeException(ex);
                                    }
                                }
                            });
                        }
                    } catch (BadLocationException ex) {
                        throw new RuntimeException(ex);
                    }
                }
            }

            @Override
            public void removeUpdate(DocumentEvent e) {
                setModified(true);
            }

            @Override
            public void changedUpdate(DocumentEvent e) {
            }
        });

        addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                if (KeyEvent.VK_HOME == e.getKeyCode()) {
                    onHome(e.getModifiers() % KeyEvent.SHIFT_DOWN_MASK != 0);
                    e.consume();
                }

                if (KeyEvent.VK_Z == e.getKeyCode() && e.isControlDown()) {
                    undo.undo();
                }
            }
        });
    }

    public int linesCount() {
        String text = null;
        try {
            text = getDocument().getText(0, getDocument().getLength());
        } catch (BadLocationException ex) {
            throw new RuntimeException(ex);
        }
        int totalCharacters = text.length();
        int lineCount = (totalCharacters == 0) ? 1 : 0;

        try {
            int offset = totalCharacters;
            while (offset > 0) {
                offset = Utilities.getRowStart(this, offset) - 1;
                lineCount++;
            }
        } catch (BadLocationException e) {
            e.printStackTrace();
        }
        return lineCount;
    }

    private void onHome(final boolean shift) {
        try {
            int newCaretPos = 0;
            int row = getSelectedRow() - 1;
            String line = getLine(row);
            int caretPos = getCaretPositionInRow();
            int textBoundPos = getIdentationOfLine(line).length();
            if (caretPos == 0) {
                newCaretPos = textBoundPos;
            } else if (caretPos <= textBoundPos) {
                newCaretPos = 0;
            } else if (caretPos > textBoundPos) {
                newCaretPos = textBoundPos;
            }

            final int carPos = getGlobalCaretPosition(row, newCaretPos);
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    if (shift) {
                        moveCaretPosition(carPos);
                    } else {
                        setCaretPosition(carPos);
                    }
                }
            });
        } catch (BadLocationException ex) {
            throw new RuntimeException(ex);
        }
    }

    private String getIdentationOfLine(String text) {
        StringBuilder sb = new StringBuilder(100);
        for (int i = 0; i < text.length(); i++) {
            char c = text.charAt(i);
            if (c == ' ' || c == '\t') {
                sb.append(c);
            } else {
                break;
            }
        }

        return sb.toString();
    }

    private String getLine(int line) {
        return getText().split("\\n")[line];
    }

    private void newRow() throws BadLocationException {
        int row = getSelectedRow();
        int rowBeforeIndex = row - 2;
        if (rowBeforeIndex < 0) {
            return;
        }
        final String text = getLine(rowBeforeIndex);
        if (text != null) {
            document.insertString(getCaretPosition(), getIdentationOfLine(text), null);
        }
    }

    public String[] getRowsBeforeAndSelectedRows() {
        String[] lines = null;
        try {
            lines = getText(0, getCaretPosition()).split("\\r?\\n");
        } catch (BadLocationException ex) {
            throw new RuntimeException(ex);
        }
        return lines;
    }

    public String getStringBeforeCaretAndStartOfRow() {
        try {
            int offs = Utilities.getRowStart(this, getCaretPosition()) - 1;
            return getText(offs + 1, getCaretPosition() - offs - 1);
        } catch (BadLocationException ex) {
            throw new RuntimeException(ex);
        }
    }

    public int getSelectedRow() {
        return getRowIndex(getCaretPosition());
    }

    public int getCaretPositionInRow() {
        try {
            int caret = getCaretPosition();
            int start = Utilities.getRowStart(this, caret);
            return caret - start;
        } catch (BadLocationException ex) {
            throw new RuntimeException(ex);
        }
    }

    private int getLineStartOffset(int line) throws BadLocationException {
        Element map = document.getDefaultRootElement();
        if (line < 0) {
            throw new BadLocationException("Negative line", -1);
        } else if (line >= map.getElementCount()) {
            throw new BadLocationException("No such line", document.getLength() + 1);
        } else {
            Element lineElem = map.getElement(line);
            return lineElem.getStartOffset();
        }
    }

    public int getGlobalCaretPosition(int row, int caretPosInLine) throws BadLocationException {
        int offset = getLineStartOffset(row);
        return offset + caretPosInLine;
    }

    public int getRowIndex(int pos) {
        int rn = (pos == 0) ? 1 : 0;
        try {
            int offs = pos;
            while (offs > 0) {
                offs = Utilities.getRowStart(this, offs) - 1;
                rn++;
            }
        } catch (BadLocationException e) {
            e.printStackTrace();
        }
        return rn;
    }

    public int getColumnIndex(int pos) {
        try {
            return pos - Utilities.getRowStart(this, pos) + 1;
        } catch (BadLocationException e) {
            e.printStackTrace();
        }
        return -1;
    }
    private boolean modified = false;

    public void clearModified() {
        modified = false;
    }

    public void setModified(boolean modified) {
        this.modified = modified;
    }

    public boolean isModified() {
        return modified;
    }

    @Override
    public void highlightLine(String type, int line, Color color) {
        LinePainter painter = painters.get(type);
        if (painter == null) {
            painter = new LinePainter(this, color);
            painters.put(type, painter);
        }

        painter.setHighLightLine(line);
        painter.setColor(color);
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                invalidate();
                repaint();
            }
        });
    }

    @Override
    public void highlight(String type, int offset, int length, Color color) {
        Object painter = highlighters.get(type);
        if (painter != null) {
            getHighlighter().removeHighlight(painter);
            highlighters.remove(painter);
        }
        try {
            Highlighter.HighlightPainter hp = new DefaultHighlighter.DefaultHighlightPainter(color);
            Object tag = this.getHighlighter().addHighlight(offset, offset + length, hp);
            highlighters.put(type, tag);
        } catch (BadLocationException ex) {
            throw new RuntimeException(ex);
        }
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                invalidate();
                repaint();
            }
        });
    }

    @Override
    public void removeHighlight(String type) {
        LinePainter painter = painters.get(type);
        if (painter != null) {
            painters.remove(type);
            painter.deregisterPainter();
            SwingUtilities.invokeLater(new Runnable() {

                @Override
                public void run() {
                    invalidate();
                    repaint();
                }
            });
            return;
        }

        Object tag = highlighters.get(type);
        if (tag != null) {
            getHighlighter().removeHighlight(tag);
            highlighters.remove(type);
        }
    }

    @Override
    public void highlightSymbol(Color color, int position) {
    }

    @Override
    public void setEnable() {
        setEditable(true);
    }

    @Override
    public void setDisable() {
        setEditable(false);
    }

    protected class MyUndoableEditListener implements UndoableEditListener {

        @Override
        public void undoableEditHappened(UndoableEditEvent e) {
            undo.addEdit(e.getEdit());
        }
    }
}
